﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MLMBonus.Engine
{
    public class BonusManager
    {
        #region Constructors
        /// <summary>
        /// No empty constructor available because we'll need to insure that we have nodes or nothing *should* work.
        /// </summary>
        private BonusManager() { }
        /// <summary>
        /// Create a new BonusManager object that will use the INode objects passed to it in subsequent calls to 
        /// Run.
        /// </summary>
        /// <param name="Nodes">
        /// The INode objects that will be used during the Run method call. The BonusManager will link the INode 
        /// objects using the ParentId property to identify the parent-child relationship. INode objects that have a
        /// null Parent or an empty <b>or</b> null ParentId will be identified as "Root" INodes for purposes of the 
        /// bonus run.
        /// </param>
        public BonusManager(IEnumerable<INode> Nodes)
        {
            foreach (INode node in Nodes)
            {
                nodes.Add(node.Id, node);
            }
        }
        /// <summary>
        /// Creates a new BonusManager object that will assign the passed Context to each INode and use the INode objects 
        /// in subsequent calls to Run.
        /// </summary>
        /// <param name="Nodes">
        /// The INode objects that will be used during the Run method call. Note that each INode's Id property should 
        /// have been used as the Key in the passed Dictionary. The BonusManager will link the INode 
        /// objects using the ParentId property to identify the parent-child relationship. INode objects that have a
        /// null Parent <b>or</b> an empty or null ParentId will be identified as "Root" INodes for purposes of the 
        /// bonus run.
        /// </param>
        /// <param name="Context">
        /// The IBonusContext object will be used to pay and promote nodes during the bonus run. It can also be used to hold 
        /// information or methods that will be "global" to the bonus run (such as pool payments).
        /// </param>
        public BonusManager(IEnumerable<INode> Nodes, IBonusContext Context)
            : this(Nodes)
        {
            foreach (INode node in Nodes)
            {
                node.Context = Context;
            }
        }
        /// <summary>
        /// Creates a new BonusManager object that will use the INode objects passed to it in subsequent calls to 
        /// Run.
        /// </summary>
        /// <param name="Nodes">
        /// The INode objects that will be used during the Run method call. Note that each INode's Id property should 
        /// have been used as the Key in the passed Dictionary. The BonusManager will link the INode 
        /// objects using the ParentId property to identify the parent-child relationship. INode objects that have a
        /// null Parent <b>or</b> an empty or null ParentId will be identified as "Root" INodes for purposes of the 
        /// bonus run.
        /// </param>
        public BonusManager(Dictionary<string, INode> Nodes)
        {
            nodes = Nodes;
        }
        /// <summary>
        /// Creates a new BonusManager object that will use the INode objects passed to it in subsequent calls to 
        /// Run.
        /// </summary>
        /// <param name="Nodes">
        /// The INode objects that will be used during the Run method call. Note that each INode's Id property should 
        /// have been used as the Key in the passed Dictionary. The BonusManager will link the INode 
        /// objects using the ParentId property to identify the parent-child relationship. INode objects that have a
        /// null Parent <b>or</b> an empty or null ParentId will be identified as "Root" INodes for purposes of the 
        /// bonus run.
        /// </param>
        /// <param name="Context">
        /// The IBonusContext object will be used to pay and promote nodes during the bonus run. It can also be used to hold 
        /// information or methods that will be "global" to the bonus run (such as pool payments).
        /// </param>
        public BonusManager(Dictionary<string, INode> Nodes, IBonusContext Context)
            : this(Nodes)
        {
            foreach (KeyValuePair<string, INode> node in Nodes)
            {
                node.Value.Context = Context;
            }
        }
        #endregion
        /// <summary>
        /// This method is called during the constructors that pass INode collections to link parents and children.
        /// </summary>
        protected void LinkNodes()
        {
            // reset in case this bonus has a previous run.
            foreach (INode node in nodes.Values)
            {
                node.Children.Clear();
                node.Parent = null;
            }

            foreach (INode node in nodes.Values)
            {
                if (!string.IsNullOrWhiteSpace(node.ParentId) && nodes.ContainsKey(node.ParentId))
                {
                    INode parent = nodes[node.ParentId];
                    parent.Children.AddLast(node);
                    node.Parent = parent;
                }
            }
        }
        #region Properties
        private Dictionary<string, INode> nodes = new Dictionary<string,INode>();
        /// <summary>
        /// The nodes to be used during a bonus run. If this property is not populated in a constructor, the calling
        /// object should populate the Children LinkedList and the Parent property of each INode. Note that this property 
        /// will be locked during a bonus run.
        /// </summary>
        public Dictionary<string, INode> Nodes
        {
            get
            {
                if (isRunning)
                {
                    throw new ArgumentException("Cannot access Nodes while a bonus is running");
                }
                else
                {
                    return nodes;
                }
            }
            set
            {
                if (isRunning)
                {
                    throw new ApplicationException("Cannot change the Nodes reference while a bonus is running");
                }
                else
                {
                    nodes = value;
                }
            }
        }

        private bool isRunning;
        /// <summary>
        /// This read-only property will be true during a bonus run.
        /// </summary>
        public bool IsRunning
        {
            get
            {
                return isRunning;
            }
        }

        public static DateTime PeriodStartDate { get; set; }
        public static DateTime PeriodEndDate { get; set; }
        #endregion
        #region Events
        /// <summary>
        /// Raised before calling PreProcess on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> PreProcessing;
        /// <summary>
        /// Raised after calling PreProcess on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> PreProcessed;
        /// <summary>
        /// Raised before calling Promote on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> Promoting;
        /// <summary>
        /// Raised after calling Promote on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> Promoted;
        /// <summary>
        /// Raised before calling Pay on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> Paying;
        /// <summary>
        /// Raised after calling Pay on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> Paid;
        /// <summary>
        /// Raised before calling PostProcess on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> PostProcessing;
        /// <summary>
        /// Raised after calling PostProcess on an INode object. The associated INode object can be accessed through 
        /// the Node property of the BonusEventArgs parameter.
        /// </summary>
        public event EventHandler<BonusEventArgs> PostProcessed;

        private void OnPreProcessing(INode node)
        {
            EventHandler<BonusEventArgs> preProcessing = PreProcessing;
            if (preProcessing != null)
            {
                preProcessing(this, new BonusEventArgs(node));
            }
        }
        private void OnPreProcessed(INode node)
        {
            EventHandler<BonusEventArgs> preProcessed = PreProcessed;
            if (preProcessed != null)
            {
                preProcessed(this, new BonusEventArgs(node));
            }
        }
        private void OnPromoting(INode node)
        {
            EventHandler<BonusEventArgs> promoting = Promoting;
            if (promoting != null)
            {
                promoting(this, new BonusEventArgs(node));
            }
        }
        private void OnPromoted(INode node)
        {
            EventHandler<BonusEventArgs> promoted = Promoted;
            if (promoted != null)
            {
                promoted(this, new BonusEventArgs(node));
            }
        }
        private void OnPaying(INode node)
        {
            EventHandler<BonusEventArgs> paying = Paying;
            if (paying != null)
            {
                paying(this, new BonusEventArgs(node));
            }
        }
        private void OnPaid(INode node)
        {
            EventHandler<BonusEventArgs> paid = Paid;
            if (paid != null)
            {
                paid(this, new BonusEventArgs(node));
            }
        }
        private void OnPostProcessing(INode node)
        {
            EventHandler<BonusEventArgs> postProcessing = PostProcessing;
            if (postProcessing != null)
            {
                postProcessing(this, new BonusEventArgs(node));
            }
        }
        private void OnPostProcessed(INode node)
        {
            EventHandler<BonusEventArgs> postProcessed = PostProcessed;
            if (postProcessed != null)
            {
                postProcessed(this, new BonusEventArgs(node));
            }
        }
        #endregion
        /// <summary>
        /// Called to begin a bonus run with a StartDate and EndDate.
        /// </summary>
        /// <param name="BonusRunType">
        /// BonusRunType is used so that multiple run types can be handled by a single bonus module. It is expected that
        /// BonusRunType be used inside the method to determine the correct logic to be run at a given time.
        /// 
        /// Note that all INode objects in the Nodes property that have an empty or null ParentId <b>or</b> a null 
        /// Parent will be treated as a root node for the purposes of the bonus run.
        /// </param>
        /// <param name="StartDate">The period start date.</param>
        /// <param name="EndDate">The period end date.</param>
        public void Run(string BonusRunType, DateTime StartDate, DateTime EndDate)
        {
            PeriodStartDate = StartDate;
            PeriodEndDate = EndDate;
            Run(BonusRunType);
        }
        /// <summary>
        /// Called to begin a bonus run.
        /// </summary>
        /// <param name="BonusRunType">
        /// BonusRunType is used so that multiple run types can be handled by a single bonus module. It is expected that
        /// BonusRunType be used inside the method to determine the correct logic to be run at a given time.
        /// 
        /// Note that all INode objects in the Nodes property that have an empty or null ParentId <b>or</b> a null 
        /// Parent will be treated as a root node for the purposes of the bonus run.
        /// </param>
        public void Run(string BonusRunType)
        {
            isRunning = true;
            // Reset all nodes 
            LinkNodes();
            nodes.Values.ToList().ForEach(n => n.IsPreProcessed = false);

            // Get all nodes that are at the top of a tree. i.e. all those without a ParentId or with a null Parent.
            IEnumerable<INode> rootNodes = nodes.Values.Where(n => string.IsNullOrEmpty(n.ParentId) || n.Parent == null);
            foreach (INode rootNode in rootNodes)
            {
                runRoot(BonusRunType, rootNode);
            }
            
            isRunning = false;
        }
        private void runRoot(string bonusRunType, INode root)
        {
            // We do *not* want recursive calls so any methods called here should return before moving on.

            LinkedListNode<INode> currentNode = new LinkedListNode<INode>(root);
            root.ParentNode = null;
            while (currentNode != null)
            {
                INode node = currentNode.Value;

                if (!node.IsPreProcessed)
                {
                    OnPreProcessing(node);
                    node.PreProcess(bonusRunType);
                    node.IsPreProcessed = true;
                    OnPreProcessed(node);

                    if (node.Children.Count > 0)
                    {
                        // Children present, set their parent node property.
                        node.Children.ToList().ForEach(c => c.ParentNode = currentNode);
                        // Move focus down.
                        currentNode = node.Children.First;
                        continue;
                    }
                }

                // Either there are no children, or children have all been processed and we've returned.
                processNode(node, bonusRunType);

                if (currentNode.Next != null)
                {
                    // There's a sib, move over.
                    currentNode = currentNode.Next;
                }
                else
                {
                    // No sibs left, move up.
                    // Note that if this is the root node, moving up will provide us with a null that 
                    // ends the while loop.
                    currentNode = node.ParentNode;
                }
            }
        }
        private void processNode(INode node, string bonusRunType)
        {
            OnPromoting(node);
            node.Promote(bonusRunType);
            OnPromoted(node);

            OnPaying(node);
            node.Pay(bonusRunType);
            OnPaid(node);

            OnPostProcessing(node);
            node.PostProcess(bonusRunType);
            OnPostProcessed(node);
        }
    }

    public class BonusEventArgs : EventArgs
    {
        public INode Node { get; set; }
        internal BonusEventArgs(INode EventNode)
        {
            Node = EventNode;
        }
    }
}
